Let's Talk About React's New Features
Introduction
In a recent interview, we talked about the major React updates in recent years, and I realized that my in-depth understanding was still at the time when Hooks
were first introduced. My recent attention to the community has only been at the level of knowing that 18.2
is out and that it has fully embraced Next.js
.
More anxiety-inducing than age for programmers is the lack of understanding of new changes.
This article is not intended to provide a detailed explanation of the new hooks. The official documentation is definitely the most detailed for that. Instead, it's mainly to write some demos to help myself understand.
Import React 18.2 in the Blog
console.log('React version:', React.version)
console.log('ReactDOM version:', ReactDOM.version)
useLayoutEffect
This is a feature that came out in version 17.x
, but I haven't used it yet.
The official description: useLayoutEffect
is a version of useEffect that fires before the browser repaints the screen.
To improve that useLayoutEffect
has a blocking effect on rendering, I wrote the following simple demo:
Clicking inside the red box
loads a small blue box, and clicking the green box
hides the small blue box.
To see the difference, I added a heavy loop, switch to click several times.
When you change the example code to useEffect
, you can clearly see a flicker.
const { createRoot } = ReactDOM
const { useEffect, useLayoutEffect, useState } = React
const root = createRoot(document.getElementById('useLayoutEffect'))
const ChildDom = ({ position }) => {
const [state, setState] = useState({ x: 0, y: 0 })
// !!!!change this to useEffect to see the difference!!!!
useLayoutEffect(() => {
for (let i = 0; i < 1e8; i += 1) {
const a = Math.random()
}
setState(position)
}, [])
return (
<div style={{
position: 'absolute',
left: state.x,
top: state.y,
width: '100px',
height: '100px',
background: 'blue'
}}>
child
</div>
)
}
const App = () => {
const [childPosition, setChildPosition] = useState(null)
return (
<div
style={{
display: 'flex'
}}
>
<div
onClick={e => {
setChildPosition({
x: e.nativeEvent.offsetX,
y: e.nativeEvent.offsetY
})
}}
style={{
border: '1px solid red',
width: '200px',
height: '200px',
position: 'relative'
}}
>
click to show area
{childPosition && (
<ChildDom
position={childPosition}
/>
)}
</div>
<div
onClick={() => setChildPosition(null)}
style={{
border: '1px solid green',
width: '200px',
height: '200px',
position: 'relative'
}}
>
click to disappear area
</div>
</div>
)
}
root.render(<App />);
There's a reason why I know but haven't used
this feature yet, as I can't immediately think of a use case for it. For example, in the demo
I wrote above, the initial state
set value inside the component can be completely avoided in another way.
The Highlights of React 18
The most talked about feature in React 18 is concurrent rendering
, which addresses what issue? Let's look at a demo
first. Try switching between the two buttons, is it laggy?
It's laggy because activeB
renders 100,000 div
.
The official documentation provides two hooks
, useTransition
and useDeferredValue
.
The useTransition
allows for concurrent UI events at the same level as rendering activeB
div, you could set a pending
UI to achieve a smoother interaction.
The useDeferredValue
defers the rendering of activeB, allowing it to be interrupted, also improving the user interaction experience.
const { createRoot } = ReactDOM
const { useState } = React
const root = createRoot(document.getElementById('demo'))
const ChildA = () => {
console.log('renderA')
return (
<div>
childA
</div>
)
}
const ChildB = () => {
console.log('render heavy B')
return (
<div>
{new Array(1e5).fill(0).map((_, i) => (
<div key={i}>
childB
</div>
))}
</div>
)
}
const App = () => {
const [activeA, setActiveA] = useState(true)
const handleClickButton = (param) => {
setActiveA(param)
}
return (
<div>
<button
style={{ color: activeA ? 'red' : 'black' }}
onClick={() => handleClickButton(true)}
>
activate A
</button>
<button
style={{ color: !activeA ? 'red' : 'black' }}
onClick={() => handleClickButton(false)}
>
activate B
</button>
{activeA ? <ChildA /> : <ChildB />}
</div>
)
}
root.render(<App />);
useTransition
In this example, after clicking activeB
, a pending UI
is "concurrently" rendered (you can also change it to a pre-activated state of the activeB button) until ChildB
is fully rendered. From a UI interaction perspective, the lagging sensation is significantly reduced.
const { createRoot } = ReactDOM
const { useTransition, useState } = React
const root = createRoot(document.getElementById('demo-useTransition'))
const ChildA = () => {
console.log('renderA')
return (
<div>
childA
</div>
)
}
const ChildB = () => {
console.log('render heavy B')
return (
<div>
{new Array(1e5).fill(0).map((_, i) => (
<div key={i}>
childB
</div>
))}
</div>
)
}
const App = () => {
const [activeA, setActiveA] = useState(true)
const [isPending, startTransition] = useTransition();
const handleClickButton = (param) => {
startTransition(() => {
setActiveA(param)
})
}
return (
<div>
<button
style={{ color: activeA ? 'red' : 'black' }}
onClick={() => handleClickButton(true)}
>
activate A
</button>
<button
style={{ color: !activeA ? 'red' : 'black' }}
onClick={() => handleClickButton(false)}
>
activate B
</button>
{isPending ? <div>loading...</div>
: activeA ? <ChildA /> : <ChildB />}
</div>
)
}
root.render(<App />);
useDeferredValue
In this example, I separated the state controlling the activation of the activeB button
and the state rendering activeB list
. The two states trigger "concurrently", I placed the heavier renderListA
in useDeferredValue
.
You can see in the example that the activeB button is activated immediately, but the list rendering is delayed.
This isn't something that can be done by simply splitting the state into two. You can try changing the renderListA
in the example to const renderListA = activeA
, it won't work as expected.
const { createRoot } = ReactDOM
const { useDeferredValue, useState, useMemo } = React
const root = createRoot(document.getElementById('demo-useDeferredValue'))
const ChildA = () => {
console.log('renderA')
return (
<div>
childA
</div>
)
}
const ChildB = () => {
console.log('render heavy B')
return (
<div>
{new Array(1e5).fill(0).map((_, i) => (
<div key={i}>
childB
</div>
))}
</div>
)
}
const App = () => {
const [activeA, setActiveA] = useState(true)
const renderListA = useDeferredValue(activeA)
return (
<div>
<button
style={{ color: activeA ? 'red' : 'black' }}
onClick={() => setActiveA(true)}
>
activate A
</button>
<button
style={{ color: !activeA ? 'red' : 'black' }}
onClick={() => setActiveA(false)}
>
activate B
</button>
{renderListA ? <ChildA /> : <ChildB />}
</div>
)
}
root.render(<App />);